类型信息
类型信息是什么?
回顾一下java的类初始化和对象初始化:
- 当首次调用静态方法,静态字段,构造器(可以看成静态方法)时,检查.class文件,若未发现,则编译源文件生成.class文件,然后加载生成Class对象,开始类初始化:按顺序执行所有的静态初始化。
- 之后就不会再做类初始化了,当调用new()的时候执行对象的初始化:先在堆上分配空间,这块空间会被清零,也就是开始赋默认值,执行非静态的初始化。
- 执行构造器。
- 将引用指向这个地址(这一步有可能重排序)
.class文件是编译后产生的。
因此:这里的类型信息指的就是在类初始化的时候,加载完.class文件后,由JVM生成的Class类型实例。
RTTI
java如何支持运行时类型检查?
- RTTI:运行时类型信息
- 反射
RTTI的应用?
JAVA中所有的类型转换,都会在运行时期做类型检查(比如判断能否转换),这就需要获取到对应的类型信息,这也是RTTI最基本的应用。
运行时类型检查是隐式的运用了RTTI
要想理解RTTI的工作原理,或者显式的使用RTTI,那么就要明白Class对象。
Class对象
如何生成和使用Class对象?
JAVA提供Class类来让程序员获取类信息,每一个类都会有一个Class对象。
如何生成Class对象。
- 使用Class.forName()
- 使用类字面常量:类.class
- 使用对象中的方法:对象.getClass()
使用getClass()是获取运行时的实际类型信息
使用.class和使用Class.forName()生成Class对象有什么不同
初始化的阶段不同,.class的类初始化(执行静态初始化块)是延迟性的,在访问他的静态方法或者非静态成员的时候才开始初始化。注意在访问静态常量的时候是不会执行
初始化的:
1 | class Initable { |
为什么使用泛化的Class?
给Class的引用加一个限定,让编译器做类型检查,这也是泛型的一个作用。
并且在使用newInstance()的时候也可以用具体类型的引用去接收。
如果我们想放宽泛型的类型检查?
使用父类为类型参数的泛型时行不通的。
分析下面两个向上转型的情形:
Number和Integer:可以向上转型
Class
解决方案是使用通配符:Class<?>
使用通配符的Class和平凡的Class的区别?
仅仅时候<?>行和平凡的Class没有任何区别,只是这样表示我们是有意为之,而不是大意疏忽。
另外,<?>可以使用边界限定来进一步限定。
类型转换前先做类型检查
为什么要做类型检查?
仅仅使用RTTI的话,可以支持类型检查,但是如果到了运行时期才发现类型不一致,会报错抛出ClassCastException异常。
如何做类型检查?
- 使用instanceOf关键词(注意,这是一个关键字。) 对象 instanceOf 类型名:对象是该类型或者子类
instanceof的限制?
这里的“类型名”无法放在容器中做迭代处理,如果需求需要计数,只是使用很多instanceOf来一一判断,这是不好的。
如何优化?
使用 Class对象.isInstance(对象):对象是该类型或者子类
这样一来可以将多个Class对象存放到容器中,做遍历。使用 Class对象 == Class对象 或者 Class对象.equle(Class对象),这两个产生的效果一样;
使用Class的话,是不涉及子类、父类的,就是类型比较。
容器中的Class对象必须写死,如何进一步优化?
- 使用 Class对象.isAssignableFrom(Class对象):后者是前者类型或者其父类。
这样一来,前一个父级Class对象可以作为参数传递进来。
注册工厂
这个知识点没弄太明白,先只介绍,案例:
1 | class Part { |
和之前在学习类型检查的时候定义的Pet和LiteralPetCreator的运用相比,有以下区别:
- 需要生成对象的列表信息存放在基类中,而不是其他的类。
- 生成对象的方式不一样,注册工厂采用了工厂模式的方法。
使用new 和 newInstance()的区别
new:效率较高,但其实是一种硬编码,耦合度较高,使用工厂方法减缓了这种硬编码,但也只是把耦合弄到了工厂中。
newInstance:所有Class都具有的方法,可以实现代码的自动化,实现完全耦合。
反射
之前讲到的RTTI都存在一个限制,那就是都可以明确我们想要处理的类型是哪一种,如果出现下面几种情况(举例):
- 工具类中的一个方法,需要传入一个Class类型的参数,这时候不能明确需要处理的类型。
- IOC编程。
- .class文件通过网络传输过来。
这些时候就需要使用反射了:
反射是使用Class和java.lang.reflect(包含Field,Method和Constructor)做支持。
RTTI和反射的区别?
RTTI实际上是一个来之C++的概念,是传统的获取类型信息的方式,获取类型名称,对比类型是否一致等等,而JAVA中的反射是一种新的获取类型信息的方式,功能比RTTI更强大,可以把使用到reflect的RTTI叫作反射,又或者都可以称之为反射,不要太纠结。
反射的应用:
- 类方法生成器
- 代理
什么是代理模式?
当您希望将额外的操作分离到与“真正的对象”不同的位置时,代理可能会很有帮助,特别是当您希望轻松地从不使用额外的操作更改为使用额外的操作时,反之亦然。
案例:
1 | interface Interface { |
上面的代理方法稍微还有点缺陷,那就是需要每次都需要人为的去创建一个代理类,其实我们关注的只是代理了哪些操作,对于代理类的名字并不是那么的重要。
因此JAVA更进一步的完善了代理,使用反射的技术,实现了动态代理。
如何实现了动态代理?
JAVA使用了Proxy类来实现动态代理的功能,如下:
1 | class DynamicProxyHandler implements InvocationHandler{ |
Proxy中的静态方法newProxyInstance(类加载器,代理的接口,代理的逻辑处理类)可以用来生成代理类:
- 类加载器:作用暂时未知,以后讨论。
- 代理的接口:一个Class数组。
- 代理的逻辑处理类,实现了InvocationHandler接口,需要提供一个带参数构造器,把真正的对象传递进去,还需要实现里面的invoke(Object proxy, Method method, Object[] args)方法,再在调用反射方法method.invoke(proxied, args)前后我们可以添加需要代理的操作。
这里有个误区,就是构造器传进去的对象就是被代理的对象,注意到被代理的对象是method.invoke()中的proxied参数表示的对象,在这之间通过构造器穿进去的对象依然可以改变。
空对象
null在程序中很容易引起问题,因此最好给一个类创建一个表示“空”的空对象,空对象实现了空接口。如下:
1 | public interface Null {} |
空对象的判断和null的判断没什么区别,只是在使用对象中的方法或者字段的时候,可以避免出现空指针异常。
接口与类型信息
通过RTII,绕过接口会增加耦合:
之前说了使用接口可以实现完全解耦,因为接口相当于一个安全机制,不会涉及到真正的类的信息,也就无法在代码中使用真正对象的其他方法,无法做关系到某个具体对象的硬编码。
但事实真的是这样的吗?其实,
只要我们在代码中看到这个接口具体的类是什么,我们就可以使用instanceOf结合转型转变成真正的类型。
这样接口解耦就毫无意义了。
如何防止这种情况?
我们可能会想,如果防止转型的发生就可以杜绝这种操作了。
如何防止转型的发生呢?
把真正的类信息设置成包类型的,这样可以杜绝转型的发生。
这样真的解决问题了吗?
通过设置访问权限我可以无法访问到真正的类型,但是只要我能知道你的字段,方法名称是什么,我还是能使用反射来调用。
我们可能回想,能不能不让他看到我们类型的字段,方法名字了?
比如只发布编译后的代码,其实也没用,因为通过反编译,我还是能知道原来的类型信息。
实际上使用反射,没有任何类信息可以隐藏起来。
如果执意使用反射,也就意味着没办法解耦了,那么也就得承受这种结果。
存在即合理,反射提供了一种后门,没准就能在某个时候解决特定的问题。